﻿using System;
using System.Collections.Generic;
using OverStore.Runtime.PersistenceServiceProvider;
using OverStore.Scoping;
using OverStore.Runtime.Storage;

namespace OverStore.Runtime.ObjectRepository.DeferredLoader
{
    /// <summary>
    /// Supports deffered loading of objects implements <see cref="IDeferredLoadingSupport"/>
    /// interface.
    /// </summary>
    /// <typeparam name="TInstance">The type of the instance.</typeparam>
    /// <typeparam name="TIdentifier">The type of the identifier.</typeparam>
    public sealed class InterfaceDeferredLoader<TInstance, TIdentifier> : IDeferredLoader<TInstance>
        where TInstance : class, IDeferredLoadingSupport
    {
        private readonly IPersistenceServiceProvider _Provider;
        private readonly IDataStorage _Storage;

        private readonly Dictionary<TIdentifier, Func<TInstance, bool>> _WaitInstances
            = new Dictionary<TIdentifier, Func<TInstance, bool>>();

        /// <summary>
        /// Initializes a new instance of the <see cref="InterfaceDeferredLoader{TInstance, TIdentifier}"/> class.
        /// </summary>
        /// <param name="provider">Object provides access to different services that allows to perform different task regarding persisting instance of specified type.</param>
        /// <param name="storage">The data storage.</param>
        /// <exception cref="ArgumentNullException"><paramref name="provider"/> is null reference.</exception>
        public InterfaceDeferredLoader(IPersistenceServiceProvider provider, IDataStorage storage)
        {
            if (Object.ReferenceEquals(provider, null))
                throw new ArgumentNullException("provider");
            if (Object.ReferenceEquals(storage, null))
                throw new ArgumentNullException("storage");
            _Storage = storage;
            _Provider = provider;
        }

        /// <summary>
        /// Gets a result indicating whether this deffered loader is active.
        /// </summary>
        /// <result>
        ///     <c>true</c> if deffered loading is enabled; otherwise, <c>false</c>, that means interoperation with <see cref="IDeferredLoader{TInstance}"/>
        /// may be bypassed.
        /// </result>
        public bool IsEnabled
        {
            get
            {
                return true;
            }
        }

        /// <summary>
        /// Register <paramref name="instance"/> for loading or load it immediately if delayed loading is not supported by
        /// specified object of specified implementation. Method returns <c>true</c> if instance is loaded successfuly or
        /// if deferred loading is enabled.
        /// </summary>
        /// <param name="instance">The instance to load.</param>
        /// <param name="loadAction">Delegate that should be called when loading required.</param>
        /// <returns></returns>
        public bool LoadOrDelay(TInstance instance, Func<TInstance, bool> loadAction)
        {
            return RegisterForDefferLoading(instance, loadAction);
        }

        /// <summary>
        /// Checks does object wait for loading on demand, or not.
        /// </summary>
        /// <param name="instance">The instance to check for waiting for loading.</param>
        /// <returns>
        ///     <c>true</c> if <paramref name="instance"/> is waiting for loading; otherwise, <c>false</c>.
        /// </returns>
        public bool IsWaitForLoading(TInstance instance)
        {
            if (instance == null)
                throw new ArgumentNullException("instance");
            var id = _Provider.GetIdentifier<TInstance, TIdentifier>(instance);
            return _WaitInstances.ContainsKey(id);
        }

        private bool RegisterForDefferLoading(TInstance instance, Func<TInstance, bool> loadAction)
        {
            instance.LoadingRequired += OnInstanceLoadingRequired;
            var id = _Provider.GetIdentifier<TInstance, TIdentifier>(instance);
            _WaitInstances[id] = loadAction;
            return true;
        }

        private void OnInstanceLoadingRequired(object sender, EventArgs e)
        {
            if (sender == null)
                throw new ArgumentNullException("sender");
            var defferLoading = sender as TInstance;
            if (Object.ReferenceEquals(defferLoading, null))
                throw new ArgumentException("The sender argument has wrong type.", "sender");
            defferLoading.LoadingRequired -= OnInstanceLoadingRequired;
            var id = _Provider.GetIdentifier<TInstance, TIdentifier>(defferLoading);
            Func<TInstance, bool> loadAction;
            if (_WaitInstances.TryGetValue(id, out loadAction))
            {
                using (var scope = _Storage.OpenScope())
                {
                    if (!loadAction(defferLoading))
                        throw new PersistingRuntimeException(
                            String.Format("Interface Deffered Loader error: reloading instance of type {0} failed: there is no data for instance in data storage.",
                                          typeof(TInstance)));
                    scope.Complete();
                }
            }
        }
    }
}